(function () {
    const OPTION = {
        BASE_UP: "up",
        BASE_DOWN: "down",
        BASE_SWAP: "swap",
        BASE_MOVETO: "moveto",
        BASE_SELECT: "select",
        BASE_FOCUS: "focus",
        BASE_FOCUS_CANCEL: "focus:cancel",
        GROUP: "group",
        SLEEP: "sleep",
        GROUPCANCEL: "group:cancel",
        SWAP: "@swap",
        SHOW: "@show",
        BACK: "@back",
    };
    const enum_postion = {
        left: Symbol(),
        right: Symbol(),
        up: Symbol(),
        down: Symbol(),
    };
    function SortCanvas(config = {}) {
        const { el, width = 1000, height = 800, layoutConfig, data } = config;

        // 画布配置
        this.width = width;
        this.height = height;
        this.el = el;

        // 绘图配置
        this.layoutConfig = {
            baseX: 300,
            baseY: 300,
            gap: 50,
            defaultBarHeight: 20,
            colBarHeight: 20,
            barWidth: 30,
            ...layoutConfig,
        };

        // 数据
        this.data = data;
        this.ctx = null;
        this.images = [];
        this.focus = null;

        // 运行
        this.speed = 10;
        this.animations = [];
        this.optionList = [];
        // 用于立即执行的运行队列
        this.optionRunList = [];
        this.animationStateId;
        this.moveConfig = {
            defaultMoveYDistace: 250,
            defaultMoveXDistace: 250,
        };

        // 对外功能
        this._pause = false;

        this.setState();
        this.create(el, width, height);
    }
    SortCanvas.prototype.setState = function () {
        Object.defineProperty(this, "pause", {
            enumerable: false,
            get() {
                return this._pause;
            },
            set(value) {
                this._pause = value;
                if (value) {
                    cancelAnimationFrame(this.animationStateId);
                } else {
                    this.render();
                }
            },
        });
    };
    SortCanvas.prototype.create = function (el, width, height) {
        if (!el) {
            el = document.body;
        } else if (typeof el === "string") {
            el = document.querySelector(el);
        }

        const layout = document.createElement("div");
        layout.setAttribute("class", "sortcanvas");

        const canvas = document.createElement("canvas");
        canvas.width = width;
        canvas.height = height;

        el.appendChild(layout);
        layout.appendChild(canvas);

        this.ctx = canvas.getContext("2d");

        this.draw();
    };
    SortCanvas.prototype.compiler = function (fn) {
        let context = fn.toString();

        // 掐头去尾
        const paramsStr = /^function.*?\((.*?)\)/.exec(context)[1];
        const funcName = /^function\s*(.*?)\(.*\)/.exec(context)[1];
        context = context.replace(/^function.*?{/, "");
        context = context.replace(/}$/, "");

        // 更换指令，不知道为啥 用"|" 不能捕获，写两遍 先
        context = context.replace(
            /\/\/\s*opt\s*(.+)\((.*)\)\s*speed\s*=\s*(\d*)/g,
            function (_, opt, params, speed) {
                return `this.addOption("${opt}", ${util.getCodeParams(
                    params
                )} ${speed ? "," + speed : ""})`;
            }
        );
        context = context.replace(
            /\/\/\s*opt\s*(.+)\((.*)\)/g,
            function (_, opt, params) {
                return `this.addOption("${opt}", ${util.getCodeParams(
                    params
                )}, this.speed)`;
            }
        );

        context = context.replaceAll(funcName, `this.$${funcName}`);
        SortCanvas.prototype["$" + funcName] = new Function(
            paramsStr,
            context
        ).bind(this);

        code = `with(this) { return $${funcName}(${paramsStr}) }`;
        return new Function(paramsStr, code).bind(this);
    };
    SortCanvas.prototype.getCompilerParams = function (start, end) {
        const res = [];
        for (let i = start; i <= end; i++) {
            res.push(i);
        }
        return res;
    };

    SortCanvas.prototype.draw = function () {
        const images = this.images;
        const data = this.data;
        const { gap, baseX, baseY } = this.layoutConfig;

        for (let i = 0; i < data.length; i++) {
            const x = gap * i + baseX;
            const y = baseY;
            images[i] = this.getImage(data[i]);
            images[i].value = data[i];
            images[i].x = x;
            images[i].y = y;

            this.pushImage(images[i], x, y);
        }
    };

    // 写得稀烂的两个执行函数

    // 其实应该叫runOneOption，只运行一个指令
    SortCanvas.prototype.run = function () {
        const optionList = this.optionList;
        const optionRunList = this.optionRunList;

        if (
            this.animationStateId !== null &&
            this.animationStateId !== undefined
        ) {
            return;
        }

        if (optionRunList.length === 0 && optionList.length === 0) {
            return;
        }
        let optionFn;
        if (optionRunList.length !== 0) {
            optionFn = optionRunList.shift();
        } else {
            optionFn = optionList.shift();
        }

        optionFn();
        this.render();
    };

    SortCanvas.prototype.render = function () {
        if (this.pause) return;
        const oneFpsAnimation = this.animations.shift();
        // end函数
        if (this.animations.length === 0) {
            cancelAnimationFrame(this.animationStateId);
            return oneFpsAnimation[0]();
        }
        if (!oneFpsAnimation) return;
        for (let i = 0; i < oneFpsAnimation.length; i++) {
            oneFpsAnimation[i]();
        }
        this.animationStateId = requestAnimationFrame(() => this.render());
    };

    // 移动指令
    SortCanvas.prototype.addOption = function (op, indexArr, speed = 5) {
        this.optionList.push(
            this.useOptionEnding(() => this.option(op, indexArr, speed))
        );
    };
    SortCanvas.prototype.option = function (op, indexArr, speed = 5) {
        switch (op) {
            case OPTION.BASE_DOWN:
                this.optionBaseDown(indexArr, speed);
                break;
            case OPTION.BASE_UP:
                this.optionBaseUp(indexArr, speed);
                break;
            case OPTION.BASE_SWAP:
                this.optionBaseSwap(indexArr, speed);
                break;
            case OPTION.BASE_FOCUS:
                this.optionBaseFocus(indexArr, speed);
                break;
            case OPTION.BASE_FOCUS_CANCEL:
                this.optionBaseFocusCancel(indexArr, speed);
                break;
            case OPTION.BASE_MOVETO:
                this.optionBaseMoveto(indexArr, speed);
                break;
            case OPTION.GROUP:
                this.optionBaseGroup(indexArr, speed);
                break;
            case OPTION.GROUPCANCEL:
                this.optionBaseGroupCancel(indexArr, speed);
                break;
            case OPTION.SLEEP:
                const sleepFpx = indexArr[0];
                this.optionBaseSleep(sleepFpx);
                break;
            // 复合指令
            case OPTION.SWAP:
                this.optionSwap(indexArr, speed);
                break;
            case OPTION.SHOW:
                this.optionShow(indexArr, speed);
                break;
            case OPTION.BACK:
                this.optionBack(indexArr, speed);
                break;
            default:
                throw new Error(`【指令异常】没有 “${op}” 移动指令`);
        }
    };
    SortCanvas.prototype.optionBaseDown = function (indexArr, speed = 5, dy) {
        indexArr = util.repeatArr(indexArr);
        let yDistace = dy || this.moveConfig.defaultMoveYDistace;
        const animations = this.animations;
        const images = this.images;
        while (yDistace > 0) {
            const oneFpsAnimation = [];
            for (let i = 0; i < indexArr.length; i++) {
                const imageData = images[indexArr[i]] || this.focus;
                oneFpsAnimation.push(() =>
                    this.moveImage(enum_postion.down, imageData, speed)
                );
            }
            animations.push(oneFpsAnimation);
            yDistace -= speed;
        }
    };
    SortCanvas.prototype.optionBaseUp = function (indexArr, speed = 5, dy) {
        indexArr = util.repeatArr(indexArr);
        let yDistace = dy || this.moveConfig.defaultMoveYDistace;
        const animations = this.animations;
        const images = this.images;
        while (yDistace > 0) {
            const oneFpsAnimation = [];
            for (let i = 0; i < indexArr.length; i++) {
                const imageData = images[indexArr[i]] || this.focus;
                oneFpsAnimation.push(() =>
                    this.moveImage(enum_postion.up, imageData, speed)
                );
            }
            animations.push(oneFpsAnimation);
            yDistace -= speed;
        }
    };
    SortCanvas.prototype.optionBaseSwap = function (indexArr, speed = 5) {
        if (indexArr.length !== 2) {
            throw new Error("【指令异常】：swap 的对象数量不为 2 ！");
        }
        const images = this.images;
        let imgLeft = images[indexArr[0]];
        let imgRight = images[indexArr[1]];
        util.swap(indexArr[0], indexArr[1], images);

        if (imgLeft.x > imgRight.x) {
            [imgLeft, imgRight] = [imgRight, imgLeft];
        }
        let xDistance = imgRight.x - imgLeft.x;

        while (xDistance > 0) {
            this.animations.push([
                () => this.moveImage(enum_postion.right, imgLeft, speed),
                () => this.moveImage(enum_postion.left, imgRight, speed),
            ]);
            xDistance -= speed;
        }
    };
    SortCanvas.prototype.optionBaseFocus = function (indexArr, speed = 5) {
        if (indexArr.length !== 1) {
            throw new Error("【指令异常】Focus 是唯一的！");
        }
        if (this.focus) {
            this.optionBaseFocusCancel([], (speed = 5));
        }
        this.focus = this.images[indexArr[0]];
        this.images[indexArr[0]] = null;
        this.optionBaseUp([-1], speed);
    };
    SortCanvas.prototype.optionBaseFocusCancel = function (_, speed = 5) {
        const { gap, baseX } = this.layoutConfig;
        const focusImage = this.focus;
        const targetIndex = this.images.indexOf(null);
        const targetX = targetIndex * gap + baseX;

        this.images[targetIndex] = this.focus;
        this.focus = null;

        const dir =
            focusImage.x < targetX ? enum_postion.right : enum_postion.left;
        let xDistance = Math.abs(focusImage.x - targetX);

        while (xDistance > 0) {
            this.animations.push([
                () => this.moveImage(dir, focusImage, speed),
            ]);
            xDistance -= speed;
        }

        this.optionBaseDown([targetIndex], speed);
    };
    SortCanvas.prototype.optionBaseMoveto = function (indexArr, speed = 5) {
        const images = this.images;
        const { gap } = this.layoutConfig;
        let imgSource = images[indexArr[0]] || this.focus;

        images[indexArr[1]] = imgSource;
        images[indexArr[0]] = null;

        let xDistance = Math.abs((indexArr[1] - indexArr[0]) * gap);
        const dir =
            indexArr[0] < indexArr[1] ? enum_postion.right : enum_postion.left;
        while (xDistance > 0) {
            this.animations.push([() => this.moveImage(dir, imgSource, speed)]);
            xDistance -= speed;
        }
    };
    SortCanvas.prototype.optionBaseGroup = function (indexArr, _) {
        indexArr = util.repeatArr(indexArr);
        const oneFpsAnimation = [];
        const color = util.getColor();
        indexArr.forEach((index) => {
            oneFpsAnimation.push(() =>
                this.setImageColor(this.images[index], color)
            );
        });
        this.animations.push(oneFpsAnimation);
    };
    SortCanvas.prototype.optionBaseGroupCancel = function (indexArr, _) {
        indexArr = util.repeatArr(indexArr);
        if (indexArr.length === 0) {
            indexArr = this.images.map((_, index) => index);
        }
        const oneFpsAnimation = [];
        const color = [255, 0, 0, 255];
        indexArr.forEach((index) => {
            oneFpsAnimation.push(() =>
                this.setImageColor(this.images[index], color)
            );
        });
        this.animations.push(oneFpsAnimation);
    };
    SortCanvas.prototype.optionBaseSleep = function (sleepFpx) {
        while (sleepFpx) {
            const oneFpsAnimation = [function () {}];
            this.animations.push(oneFpsAnimation);
            sleepFpx--;
        }
    };
    // 复合指令
    SortCanvas.prototype.optionSwap = function (indexArr, speed = 5) {
        const { baseY } = this.layoutConfig;
        const { defaultMoveYDistace } = this.moveConfig;

        this.immediatelyRun(() => {
            this.optionToTargetPositionY(
                indexArr,
                speed,
                baseY + defaultMoveYDistace
            );
        });
        this.immediatelyRun(() => {
            this.optionBaseSwap(indexArr, speed);
        });
        this.immediatelyRun(() => {
            this.optionToTargetPositionY(indexArr, speed, baseY);
        });
    };
    SortCanvas.prototype.optionShow = function (indexArr, speed = 5) {
        const { baseY } = this.layoutConfig;
        const { defaultMoveYDistace } = this.moveConfig;
        this.optionToTargetPositionY(
            indexArr,
            speed,
            baseY + defaultMoveYDistace
        );
    };
    SortCanvas.prototype.optionBack = function (indexArr, speed = 5) {
        indexArr = util.repeatArr(indexArr);
        if (indexArr.length === 0) {
            indexArr = this.images.map((_, index) => index);
        }
        this.optionToTargetPositionY(indexArr, speed);
    };
    SortCanvas.prototype.optionToTargetPositionY = function (
        indexArr,
        speed = 5,
        dy
    ) {
        indexArr = util.repeatArr(indexArr);

        const targetY = dy || this.layoutConfig.baseY;
        let inTopList = [],
            inBottomList = [];

        indexArr.forEach((index) => {
            const imageData = this.images[index];
            const dy = imageData.y - targetY;
            if (dy < 0) {
                inTopList.push({
                    dy: -dy,
                    index,
                });
            } else if (dy > 0) {
                inBottomList.push({
                    dy,
                    index,
                });
            }
        });

        while (inTopList.length) {
            const minDistance = Math.min(...inTopList.map((item) => item.dy));
            const runIndexList = [];
            inTopList.forEach((item) => {
                item.dy -= minDistance;
                runIndexList.push(item.index);
            });
            this.optionBaseDown(runIndexList, speed, minDistance);
            inTopList = inTopList.filter((item) => item.dy > 0);
        }

        while (inBottomList.length) {
            const minDistance = Math.min(
                ...inBottomList.map((item) => item.dy)
            );
            const runIndexList = [];
            inBottomList.forEach((item) => {
                item.dy -= minDistance;
                runIndexList.push(item.index);
            });
            this.optionBaseUp(runIndexList, speed, minDistance);
            inBottomList = inBottomList.filter((item) => item.dy > 0);
        }
    };

    // hook
    SortCanvas.prototype.useOptionEnding = function (fn) {
        return () => {
            fn();
            this.animations.push([
                () => {
                    // console.log("beforeOptionEnd")
                    this.animationStateId = null;
                    this.run();
                    // console.log("afterOptionEnd")
                },
            ]);
        };
    };
    SortCanvas.prototype.immediatelyRun = function (fn) {
        this.optionRunList.push(this.useOptionEnding(fn));
    };

    // 基础方法
    SortCanvas.prototype.getImage = function (value) {
        const ctx = this.ctx;
        const { defaultBarHeight, colBarHeight, barWidth } = this.layoutConfig;
        const imageData = ctx.createImageData(
            barWidth,
            defaultBarHeight + value * colBarHeight
        );
        for (let i = 0; i < imageData.data.length; i += 4) {
            imageData.data[i] = 255;
            imageData.data[i + 1] = 0;
            imageData.data[i + 2] = 0;
            imageData.data[i + 3] = 255;
        }
        return imageData;
    };
    SortCanvas.prototype.setImageColor = function (
        imageData,
        rgbaArr = [255, 0, 0, 255]
    ) {
        const [r, g, b, a] = rgbaArr;
        for (let i = 0; i < imageData.data.length; i += 4) {
            imageData.data[i] = r;
            imageData.data[i + 1] = g;
            imageData.data[i + 2] = b;
            imageData.data[i + 3] = a;
        }
        this.pushImage(imageData);
    };
    SortCanvas.prototype.pushImage = function (imageData) {
        const ctx = this.ctx;
        ctx.putImageData(imageData, imageData.x, imageData.y);
    };
    SortCanvas.prototype.clearSelf = function (imageData) {
        const ctx = this.ctx;
        const { x, y, width, height } = imageData;
        ctx.clearRect(x, y, width, height);
    };
    SortCanvas.prototype.moveImage = function (dirOp, imageData, speed = 5) {
        this.clearSelf(imageData);
        switch (dirOp) {
            case enum_postion.left:
                imageData.x = imageData.x - speed;
                break;
            case enum_postion.right:
                imageData.x = imageData.x + speed;
                break;
            case enum_postion.up:
                imageData.y = imageData.y - speed;
                break;
            case enum_postion.down:
                imageData.y = imageData.y + speed;
                break;
        }
        this.pushImage(imageData);
    };

    const util = {
        swap(i, j, arr) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
        },

        repeatArr(arr) {
            return [...new Set(arr)];
        },

        getColor() {
            const r = (Math.random() * 255) | 0;
            const g = (Math.random() * 255) | 0;
            const b = (Math.random() * 255) | 0;
            const a = 255;
            return [r, g, b, a];
        },

        getCodeParams(str) {
            if (/(.*)\s*->\s*(.*)/.test(str)) {
                return str.replace(
                    /(.*)\s*->\s*(.*)/,
                    function (_, start, end) {
                        return `this.getCompilerParams(${start}, ${end})`;
                    }
                );
            }
            if (str === "") return `[]`;
            return `[${str}]`;
        },
    };
    window.SortCanvas = SortCanvas;
})();
